/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.imaging.examples.tiff;

import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_CCITT_1D;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_CCITT_GROUP_3;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_CCITT_GROUP_4;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_DEFLATE_ADOBE;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_DEFLATE_PKZIP;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_LZW;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_PACKBITS;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_UNCOMPRESSED;
import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.COMPRESSION_UNCOMPRESSED_1;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Formatter;

import org.apache.commons.imaging.FormatCompliance;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.bytesource.ByteSource;
import org.apache.commons.imaging.formats.tiff.TiffContents;
import org.apache.commons.imaging.formats.tiff.TiffDirectory;
import org.apache.commons.imaging.formats.tiff.TiffField;
import org.apache.commons.imaging.formats.tiff.TiffReader;
import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;

/**
 * Provides methods to collect data for a tiff file. This class is intended for use with SurveyTiffFolder, though it could be integrated into other
 * applications.
 */
public class SurveyTiffFile {

    /**
     * Formats a header allowing space for the maximum length of the file paths in the list. If the comma-separated-value option is set, spaces will be
     * suppressed and commas introduced as separators.
     *
     * @param maxPathLen the maximum length of a file path (used if csv option is not set)
     * @param csv        true if formatting is configured for comma-separated-value files.
     * @return a valid string.
     */
    String formatHeader(final int maxPathLen, final boolean csv) {
        // After some false starts, it turned out that the easiest
        // way to do this is just to create a regular header and then
        // search-and-replace spaces with comma as appropriate.
        int n = maxPathLen;
        if (n < 10) {
            n = 10;
        }
        final int k0 = (n - 4) / 2;
        final int k1 = n - 4 - k0;

        final String header = String.format("%" + k0 + "sPath%" + k1 + "s%s", "", "",
                "    Size     Layout  Blk_sz     P_conf  Compress  " + "Predict  Data_Fmt   B/P B/S      Photo     ICC_Pro");
        if (csv) {
            return reformatHeaderForCsv(header);
        }
        return header;
    }

    String getBitsPerSampleString(final int[] bitsPerSample) {
        final StringBuilder s = new StringBuilder();
        for (int i = 0; i < bitsPerSample.length; i++) {
            if (i > 0) {
                s.append(".");
            }
            s.append(Integer.toString(bitsPerSample[i], 10));
        }
        return s.toString();
    }

    private String getCompressionString(final TiffDirectory directory) throws ImagingException {
        final short compressionFieldValue;
        if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) {
            compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION);
        } else {
            compressionFieldValue = COMPRESSION_UNCOMPRESSED_1;
        }
        final int compression = 0xffff & compressionFieldValue;
        switch (compression) {
        case COMPRESSION_UNCOMPRESSED: // None;
            return "None";
        case COMPRESSION_CCITT_1D: // CCITT Group 3 1-Dimensional Modified
            // Huffman run-length encoding.
            return "CCITT_1D";
        case COMPRESSION_CCITT_GROUP_3:
            return "CCITT_3";
        case COMPRESSION_CCITT_GROUP_4:
            return "CCITT_4";
        case COMPRESSION_LZW:
            return "LZW";
        case COMPRESSION_PACKBITS:
            return "PACKBITS";
        case COMPRESSION_DEFLATE_ADOBE:
        case COMPRESSION_DEFLATE_PKZIP:
            return "Deflate";
        default:
            return "None";
        }
    }

    String getIccProfileString(final TiffDirectory directory) throws ImagingException {
        final byte[] b = directory.getFieldValue(TiffEpTagConstants.EXIF_TAG_INTER_COLOR_PROFILE, false);
        if (b == null || b.length == 0) {
            return "N";
        }
        return "Y";
    }

    private String getPhotometricInterpreterString(final TiffDirectory directory, final int[] bitsPerSample) throws ImagingException {
        final int photometricInterpretation = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION);

        switch (photometricInterpretation) {
        case 0:
            return "BiLev Inv";
        case 1:
            return "BiLevel";
        case 2:
            String a = "RGB";
            if (bitsPerSample.length == 4) {
                final Object o = directory.getFieldValue(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES);
                short extraSamples = 0;
                if (o instanceof Short) {
                    extraSamples = (Short) o;
                }
                if (extraSamples == 1) {
                    a = "RGB Pre-A";
                } else {
                    a = "RGBA";
                }
            }

            return a;
        case 3:
            return "Palette";

        case 5: // CMYK
            return "CMYK";
        case 6:
            return "YCbCr";
        case 8:
            return "CieLab";

        case 32844:
        case 32845:
            return "LogLuv";
        default:
            return "Unknown";

        }

    }

    String getPlanarConfigurationString(final TiffDirectory directory) throws ImagingException {

        // Obtain the planar configuration
        final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION);
        final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY
                : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue());

        if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
            return "Chunky";
        }
        return "Planar";
    }

    String getPredictorString(final TiffDirectory directory) throws ImagingException {
        int predictor = -1;

        final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR);
        if (null != predictorField) {
            predictor = predictorField.getIntValueOrArraySum();
        }

        switch (predictor) {
        case TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING:
            return "Diff";
        case TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING:
            return "FP Diff";
        default:
            return "None";

        }
    }

    String getSampleFormatString(final TiffDirectory directory) throws ImagingException {
        final short[] sSampleFmt = directory.getFieldValue(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, false);
        if (sSampleFmt == null || sSampleFmt.length == 0) {
            return "Unknown";
        }
        String heterogeneous = "";
        for (int i = 1; i < sSampleFmt.length; i++) {
            if (sSampleFmt[i] != sSampleFmt[0]) {
                heterogeneous = "*";
                break;
            }
        }
        final int test = sSampleFmt[0];
        switch (test) {
        case TiffTagConstants.SAMPLE_FORMAT_VALUE_COMPLEX_INTEGER:
            return "Comp I" + heterogeneous;
        case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT:
            return "Float" + heterogeneous;
        case TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_COMPLEX_FLOAT:
            return "Comp F" + heterogeneous;
        case TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER:
            return "Sgn Int" + heterogeneous;
        case TiffTagConstants.SAMPLE_FORMAT_VALUE_UNSIGNED_INTEGER:
            return "Uns Int" + heterogeneous;
        default:
            return "Unknown" + heterogeneous;
        }
    }

    /**
     * Prints the legend information to the output stream
     *
     * @param ps a valid instance
     */
    void printLegend(final PrintStream ps) {
        ps.println("Legend:");
        ps.println("  Size      Size of image (width-by-height)");
        ps.println("  Layout    Organization of the image file (strips versus tiles)");
        ps.println("  Blk_sz    Size of internal image blocks (strips versus tiles)");
        ps.println("  P_conf    Planar configuration, Chunky (interleaved samples) versus Planar ");
        ps.println("  Compress  Compression format");
        ps.println("  Predict   Predictor");
        ps.println("  Data_Fmt  Data format");
        ps.println("  B/P       Bits per pixel");
        ps.println("  B/S       Bits per sample");
        ps.println("  Photo     Photometric Interpretation (pixel color type)");
        ps.println("  ICC_Pro   Is ICC color profile supplied");
        ps.println("");
        ps.println("  RGBA       RGB with unassociated alpha (transparency)");
        ps.println("  RGBA_Pre-A RGB with associated (premultiplied) alpha");
        ps.println("");
    }

    /**
     * Reformats the header inserting commas and removing spaces
     *
     * @param s a valid string
     * @return a header suitable for a CSV file.
     */
    private String reformatHeaderForCsv(final String s) {
        final StringBuilder sb = new StringBuilder(s.length());
        boolean enableComma = false;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (Character.isWhitespace(c)) {
                if (enableComma) {
                    enableComma = false;
                    sb.append(',');
                }
            } else {
                enableComma = true;
                if (Character.isUpperCase(c)) {
                    c = Character.toLowerCase(c);
                }
                sb.append(c);
            }
        }
        return sb.toString();
    }

    public String surveyFile(final File file, final boolean csv) throws ImagingException, IOException {
        String delimiter = "  ";
        if (csv) {
            delimiter = ", ";
        }

        final StringBuilder sb = new StringBuilder();
        try (Formatter fmt = new Formatter(sb)) {

            // Establish a TiffReader. This is just a simple constructor that
            // does not actually access the file. So the application cannot
            // obtain the byteOrder, or other details, until the contents have
            // been read. Then read the directories associated with the
            // file by passing in the byte source and options.
            final ByteSource byteSource = ByteSource.file(file);
            final TiffReader tiffReader = new TiffReader(true);
            final TiffContents contents = tiffReader.readDirectories(byteSource, false, // read image data, if present
                    FormatCompliance.getDefault());

            if (contents.directories.isEmpty()) {
                throw new ImagingException("No Image File Directory (IFD) found");
            }
            final TiffDirectory directory = contents.directories.get(0);

            // Get the metadata (Tags) and write them to standard output
            final boolean hasTiffImageData = directory.hasTiffImageData();
            if (!hasTiffImageData) {
                throw new ImagingException("No image data in file");
            }

            final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH);
            final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);

            int samplesPerPixel = 1;
            final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL);
            if (samplesPerPixelField != null) {
                samplesPerPixel = samplesPerPixelField.getIntValue();
            }
            int[] bitsPerSample = { 1 };
            int bitsPerPixel = samplesPerPixel;
            final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE);
            if (bitsPerSampleField != null) {
                bitsPerSample = bitsPerSampleField.getIntArrayValue();
                bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum();
            }
            if (samplesPerPixel != bitsPerSample.length) {
                throw new ImagingException("Tiff: samplesPerPixel (" + samplesPerPixel + ")!=fBitsPerSample.length (" + bitsPerSample.length + ")");
            }

            int rowsPerStrip = 0;
            int tileWidth = 0;
            int tileHeight = 0;

            final boolean imageDataInStrips = directory.imageDataInStrips();
            if (imageDataInStrips) {
                final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP);
                rowsPerStrip = Integer.MAX_VALUE;
                if (null != rowsPerStripField) {
                    rowsPerStrip = rowsPerStripField.getIntValue();
                } else {
                    final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
                    /*
                     * if rows per strip not present then rowsPerStrip is equal to imageLength or an infinity value;
                     */
                    if (imageHeight != null) {
                        rowsPerStrip = imageHeight.getIntValue();
                    }
                }
            } else {
                final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH);
                if (null == tileWidthField) {
                    throw new ImagingException("Can't find tile width field.");
                }
                tileWidth = tileWidthField.getIntValue();
                final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH);
                if (null == tileLengthField) {
                    throw new ImagingException("Can't find tile length field.");
                }
                tileHeight = tileLengthField.getIntValue();
            }

            final String compressionString = getCompressionString(directory);
            final String predictorString = getPredictorString(directory);
            final String planarConfigurationString = getPlanarConfigurationString(directory);
            final String bitsPerSampleString = getBitsPerSampleString(bitsPerSample);
            final String sampleFmtString = getSampleFormatString(directory);
            final String piString = getPhotometricInterpreterString(directory, bitsPerSample);
            final String iccString = getIccProfileString(directory);

            fmt.format("%s%4dx%-4d", delimiter, width, height);
            if (imageDataInStrips) {
                fmt.format("%sStrips%s%4dx%-4d", delimiter, delimiter, width, rowsPerStrip);
            } else {
                fmt.format("%sTiles %s%4dx%-4d", delimiter, delimiter, tileWidth, tileHeight);
            }

            fmt.format("%s%s", delimiter, planarConfigurationString);
            fmt.format("%s%-8s", delimiter, compressionString);
            fmt.format("%s%-7s", delimiter, predictorString);
            fmt.format("%s%-8s", delimiter, sampleFmtString);
            fmt.format("%s%3d", delimiter, bitsPerPixel);
            fmt.format("%s%-7s", delimiter, bitsPerSampleString);
            fmt.format("%s%-9s", delimiter, piString);
            fmt.format("%s%-7s", delimiter, iccString);

            if (csv) {
                return trimForCsv(sb);
            }
            return sb.toString();
        }
    }

    /**
     * Trims spaces from a range of characters intended for a CSV output
     *
     * @param source the standard source file
     * @return the equivalent string with spaces removed.
     */
    private String trimForCsv(final StringBuilder source) {
        int n = source.length();
        final StringBuilder sb = new StringBuilder(n);
        boolean spaceEnabled = false;
        boolean spacePending = false;
        for (int i = 0; i < n; i++) {
            final char c = source.charAt(i);
            if (Character.isWhitespace(c)) {
                if (spaceEnabled) {
                    spacePending = true;
                    spaceEnabled = false;
                }
            } else {

                if (Character.isLetter(c) || Character.isDigit(c)) {
                    if (spacePending) {
                        sb.append(' ');
                        spacePending = false;
                    }
                    spaceEnabled = true;
                } else {
                    spacePending = false;
                    spaceEnabled = false;
                }
                sb.append(c);
            }
        }
        n = sb.length();
        if (n > 0 && sb.charAt(n - 1) == ' ') {
            sb.setLength(n - 1);
        }
        return sb.toString();
    }
}
